package com.loong.android.socket;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * Socket服务
 * 需要权限：android.permission.INTERNET
 */
public class SocketServer {
    private int bufferLen = 4096;
    private ServerSocket serverSocket;
    private SocketEvent socketEvent;
    private boolean autoDealBuffer = true;
    private boolean isListen = false;   //线程监听标志位
    private final Map<String, ServerSocketThread> clientSocketMap = new HashMap<>();

    /**
     * 置缓存区尺寸，默认为4096
     *
     * @param bufferLen 字节数
     */
    public void setBufferLen(int bufferLen) {
        this.bufferLen = bufferLen;
    }

    /**
     * 置套接字事件
     *
     * @param socketEvent 事件回调
     */
    public void setSocketEvent(SocketEvent socketEvent) {
        this.socketEvent = socketEvent;
    }

    /**
     * 设置接收数据时，是否自动处理。若设置为false，则发送数据需要使用send2函数，发送不经过任何加工的原始数据。
     * 若设置为true，则发送数据需要使用send函数，发送数据时，在数据前带上4字节的数据表示数据长度。在收到数据时，
     * 先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     *
     * @param autoDealBuffer 是否启用
     */
    public void setAutoDealBuffer(boolean autoDealBuffer) {
        this.autoDealBuffer = autoDealBuffer;
    }

    /**
     * 发送数据，自动在数据前加上数据长度。
     * 在收到数据时，先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     * @param clientTag 客户端标识
     * @param data 待发送的数据
     * @param event 发送完毕的事件回调
     */
    public void send(String clientTag, byte[] data, SendEvent event) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return;
        thread.send(data, event);
    }

    /**
     * 发送数据，自动在数据前加上数据长度。
     * 在收到数据时，先读取数据长度值，再根据数据长度读取数据，这样可以解决数据传输过程的半包、粘包等问题。
     * @param clientTag 客户端标识
     * @param data 待发送的数据
     */
    public void send(String clientTag, byte[] data) {
        send(clientTag, data, null);
    }

    /**
     * 发送原始数据，需要调用setAutoDealBuffer(false)，关闭数据自动处理，否则会导致接收数据错乱。
     *
     * @param clientTag 客户端标识
     * @param data 待发送的数据
     * @param event 发送完毕的事件回调
     */
    public void send2(String clientTag, byte[] data, SendEvent event) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return;
        thread.send2(data, event);
    }

    /**
     * 发送原始数据，需要调用setAutoDealBuffer(false)，关闭数据自动处理，否则会导致接收数据错乱。
     *
     * @param clientTag 客户端标识
     * @param data 待发送的数据
     */
    public void send2(String clientTag, byte[] data) {
        send2(clientTag, data, null);
    }

    /**
     * 取已连接的客户端的IP地址
     *
     * @param clientTag 客户端标识
     */
    public String getClientIp(String clientTag) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return null;
        return thread.getSocketIp();
    }

    /**
     * 取已连接的客户端的端口号
     *
     * @param clientTag 客户端标识
     */
    public int getClientPort(String clientTag) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return 0;
        return thread.getSocketPort();
    }

    /**
     * 取已连接的客户端的Socket对象
     *
     * @param clientTag 客户端标识
     */
    public Object getClientSocket(String clientTag) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return null;
        return thread.getSocket();
    }

    /**
     * 启动服务
     *
     * @param port 端口号，范围：0~65535
     * @param timeout 超时，单位为ms，默认值为5000，即5s
     */
    public void start(int port, int timeout) {
        isListen = true;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    serverSocket = new ServerSocket(port);
                    serverSocket.setSoTimeout(timeout);
                    while (isListen) {
                        Socket socket = getSocket(serverSocket);
                        if (socket != null) {
                            ServerSocketThread st = new ServerSocketThread(socket);
                            String clientIp = st.getSocketIp();
                            int clientPort = st.getSocketPort();
                            String clientTag = clientIp + ":" + clientPort;
                            clientSocketMap.put(clientTag, st);
                            if (socketEvent != null) socketEvent.onClientConnected(clientTag, clientIp, clientPort);
                        }
                    }
                    close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 启动服务
     *
     * @param port 端口号
     */
    public void start(int port) {
        start(port, 5000);
    }

    /**
     * 停止服务
     */
    public void close() {
        isListen = false;
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        for (Map.Entry<String, ServerSocketThread> list : clientSocketMap.entrySet()) {
            list.getValue().close();
        }
        clientSocketMap.clear();
    }

    /**
     * 判断服务是否已关闭
     */
    public boolean isClosed() {
        return serverSocket == null || serverSocket.isClosed();
    }

    /**
     * 判断服务是否成功绑定到地址
     */
    public boolean isBound() {
        return serverSocket != null && serverSocket.isBound();
    }

    /**
     * 断开与某个客户端的连接
     *
     * @param clientTag 客户端标识
     */
    public boolean disconnect(String clientTag) {
        ServerSocketThread thread;
        if (!clientSocketMap.containsKey(clientTag) || (thread = clientSocketMap.get(clientTag)) == null) return false;
        thread.close();
        clientSocketMap.remove(clientTag);
        return true;
    }

    private Socket getSocket(ServerSocket serverSocket) {
        try {
            return serverSocket.accept();
        } catch (IOException e) {
            //e.printStackTrace();
            return null;
        }
    }

    public abstract static class SocketEvent {
        /**
         * 客户端接入。注意，本事件在多线程中触发，不能直接操作UI。
         *
         * @param clientTag 客户端标识，由：IP地址+":"+端口号组成。
         * @param ip IP地址
         * @param port 端口号
         */
        public void onClientConnected(String clientTag, String ip, int port) {

        }

        /**
         * 客户端断开。注意，本事件在多线程中触发，不能直接操作UI。
         *
         * @param clientTag 客户端标识，由：IP地址+":"+端口号组成。
         * @param ip IP地址
         * @param port 端口号
         */
        public void onClientDisconnected(String clientTag, String ip, int port) {

        }

        /**
         * 收到数据。注意，本事件在多线程中触发，不能直接操作UI。
         *
         * @param clientTag 客户端标识，由：IP地址+":"+端口号组成。
         * @param data 数据
         */
        public void onReceive(String clientTag, byte[] data) {

        }
    }

    /**
     * 发送数据的接口事件
     */
    public abstract static class SendEvent {
        /**
         * 数据发送完毕事件。注意，本事件在多线程中触发，不能直接操作UI。
         */
        public void onComplete(boolean res) {

        }
    }

    class ServerSocketThread extends Thread {
        Socket socket;
        private InputStream is;
        private OutputStream os;
        private DataInputStream dis;
        private boolean isRun = true;
        private final String ip;
        private final int port;
        private final String tag;
        //需要接收数据的长度
        private int needReceiveLen = 0;
        //处理步骤，0=读数据长度；1=读数据
        private int dealStep = 0;
        private byte[] dataBuffer;
        private final byte[] lenBuffer = new byte[4];
        private int dbIndex = 0;
        private int lenIndex = 0;

        ServerSocketThread(Socket socket) {
            this.socket = socket;
            this.ip = getSocketIp();
            this.port = getSocketPort();
            this.tag = ip + ":" + port;
            try {
                os = this.socket.getOutputStream();
                is = this.socket.getInputStream();
                dis = new DataInputStream(is);
                start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public Socket getSocket() {
            return this.socket;
        }

        public String getSocketIp() {
            String tmp = this.socket.getInetAddress().toString();
            return tmp.startsWith("/") ? tmp.substring(1) : tmp;
        }

        public int getSocketPort() {
            return this.socket.getPort();
        }

        public void send2(final byte[] data, SendEvent event) {
            if (os == null || data == null) {
                if(event != null) event.onComplete(false);
                return;
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        os.write(data);
                        os.flush(); //强制送出数据
                        if(event != null) event.onComplete(true);
                    } catch (IOException e) {
                        e.printStackTrace();
                        if(event != null) event.onComplete(false);
                    }
                }
            }).start();
        }

        /**
         * 在数据前加4字节数据表示数据长度，接收数据时通过数据长度拼接数据，解决数据传输过程的半包、粘包的问题
         *
         * @param data 待发送的数据
         */
        public void send(final byte[] data, SendEvent event) {
            if (os == null || data == null) {
                if(event != null) event.onComplete(false);
                return;
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        int sendLen = data.length;
                        //给数据添加包头
                        byte[] senBuffer = new byte[4 + sendLen];
                        System.arraycopy(DataUtil.intToBytes(sendLen), 0, senBuffer, 0, 4);
                        System.arraycopy(data, 0, senBuffer, 4, sendLen);
                        os.write(senBuffer);
                        os.flush(); //强制送出数据
                        if(event != null) event.onComplete(true);
                    } catch (IOException e) {
                        e.printStackTrace();
                        if(event != null) event.onComplete(false);
                    }
                }
            }).start();
        }

        public void close() {
            isRun = false;
            try {
                if (is != null) is.close();
                if (os != null) os.close();
                if (dis != null) dis.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private void dealBuffer(byte[] data, int dataLen) {
            //如果不需要自动处理分包，则直接返回。
            if (!autoDealBuffer) {
                byte[] output = new byte[dataLen];
                System.arraycopy(data, 0, output, 0, dataLen);
                if (socketEvent != null) socketEvent.onReceive(tag, output);
                return;
            }
            //处理的数据位置
            int dealIndex = 0;
            while (dealIndex < dataLen) {
                if(dealStep == 0) {
                    int tmpLen = Math.min(4 - lenIndex, dataLen - dealIndex);
                    System.arraycopy(data, dealIndex, lenBuffer, lenIndex, tmpLen);
                    lenIndex += tmpLen;
                    dealIndex += tmpLen;
                    if(lenIndex == 4) {
                        needReceiveLen = DataUtil.bytesToInt(lenBuffer);
                        if (needReceiveLen >= 0) dataBuffer = new byte[needReceiveLen];
                        dbIndex = 0;
                        lenIndex = 0;
                        dealStep = 1;
                    }
                }else {
                    int tmpLen = Math.min(needReceiveLen - dbIndex, dataLen - dealIndex);
                    System.arraycopy(data, dealIndex, dataBuffer, dbIndex, tmpLen);
                    dbIndex += tmpLen;
                    dealIndex += tmpLen;
                    if (dbIndex >= needReceiveLen) {
                        dealStep = 0;
                        if (socketEvent != null) socketEvent.onReceive(tag, dataBuffer);
                    }
                }
            }
        }

        @Override
        public void run() {
            byte[] buffer = new byte[bufferLen];
            int rcvLen;
            while (isRun && !socket.isClosed() && !socket.isInputShutdown()) {
                try {
                    if ((rcvLen = dis.read(buffer)) != -1) {
                        dealBuffer(buffer, rcvLen);
                    } else {
                        isRun = false;
                        if (socketEvent != null) socketEvent.onClientDisconnected(tag, ip, port);
                        clientSocketMap.remove(tag);
                        break;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            close();
        }
    }
}
